shell 是一个管理进程和一些程序的程序. 所有常用的 shell 都有三个主要功能
  1. 运行程序
  2. 管理输入和输出
  3. 可编程

shell 是如何运行程序的

shell 打印提示符, 输入命令, shell 就运行这个命令, 然后 shell 再次都有提示符 ---- 如此反复.

shell 的主循环

while(!end_of_input)
    get command
    execute command
    wait for command to finish

问题1: 一个程序如何运行另一个程序

答案: 程序调用 execvp

  1. 程序调用 execvp
  2. 内核从磁盘将程序载入
  3. 内核将 arglist 复制到进程
  4. 内核调用 main(argc, argv)

下面是运行 ls -l 的完整程序:

/* exec1.c - shows how easy it is for a program to run a program
 */
#include <stdio.h>
#include <unistd.h>

int main()
{
    char *arglist[3];

    arglist[0] = "ls";
    arglist[1] = "-l";
    arglist[2] = 0;
    printf("*** About to exec ls -l\n");
    execvp("ls", arglist);
    printf("*** ls is done. bye\n");
    return 0;
}

execvp 有两个参数: 要运行的程序名和那个程序的命令行参数数组. 注意, 将数组的第一个元素置为程序的名称. 还要注意, 最后一个元素必须是null.

第二条消息到哪里去了?

execvp 将程序从磁盘载入到进程以便它可以被运行,内核将新程序载入到当前进程, 替代当前进程的代码和数据.

  execvp
目标 在指定路径中查找并执行一个文件
头文件 #include <unistd.h>
原型 result execvp(const char * file, const char *argv[])
参数 file 要执行的文件名, argv 字符串数组
返回值 -1 出错

带提示符的shell, psh1.c

/* prompting shell version 1
 * Prompts for command and its arguments
 * Builds the argument vector for the call to execvp
 * Uses execvp(), and never returns.
 */

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>

#define MAXARGS 20
#define ARGLEN  100

int execute(char * arglist[]);

int main()
{
    char *arglist[MAXARGS+1];
    int numargs;
    char argbuf[ARGLEN];
    char *makestring();

    numargs = 0;

    while(numargs < MAXARGS){
        printf("Arg[%d]?", numargs);
        if (fgets(argbuf, ARGLEN, stdin) && *argbuf != '\n')
            arglist[numargs++] = makestring(argbuf);
        else{
            if (numargs > 0){
                arglist[numargs] = NULL;
                execute(arglist);
                numargs = 0;
            }
        }
    }
    return 0;
}

int execute(char *arglist[])
    /*
     * use execvp to do it
     */
{
    int b;
    b = execvp(arglist[0], arglist);
    perror("execvp failed");
    exit(1);
    return b;
}

char *makestring(char *buf)
    /*
     * trim off new line and create storage for the string
     */
{
    char *cp;
    buf[strlen(buf) - 1] = '\0';
    cp = malloc(strlen(buf) + 1);
    if (cp == NULL){
        fprintf(stderr, "no memory\n");
        exit(1);
    }
    strcpy(cp, buf);
    return cp;
}

这个版本的 sh 只能运行一个命令然后就退出了, 可以启动一个新的进程, 有这个 进程来执行命令程序.

问题2: 如何建立新的进程

答案: 一个进程调用 fork 来复制自己.

用法: fork(); /* takes no arguments */

进程拥有程序和当前运行的位置. 程序调用 fork, 当控制转移到内核中的 fork 代码 后, 内核做:

  1. 分配新的内存块和内核数据结构
  2. 复制原来的进程到新的进程
  3. 向运行的进程集添加新的进程
  4. 将控制返回给两个进程

当一个进程调用 fork 之后, 就有两个二进制代码相同的进程. 而且他们都运行到 相同的地方. 但是每个进程都将可以开始他们自己的旅程.

/* forkdemo1.c
 * shows how fork creates two processes, distinguishable
 * by the different return values form fork()
 */
#include <unistd.h>

#include <stdio.h>
int main()
{
    int ret_form_fork, mypid;
    mypid = getpid();
    printf("Before: mypid is %d\n", mypid);
    ret_form_fork = fork();
    sleep(1);
    printf("After: my pid is %d, fork() said %d\n",
            getpid(), ret_form_fork);
    return 0;
}
输出为:
Before: mypid is 5131
After: my pid is 5131, fork() said 5132
After: my pid is 5132, fork() said 0

forkdemo2.c

/* forkdemo2.c - shows how child processes pick up at the return
 *          from fork() and then can execute any code they like,
 *          even fork(). Predict number of lines of outpu.
 */
#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("my pid is %d\n", getpid());
    fork();
    fork();
    fork();
    printf("my pid is %d\n", getpid());
    return 0;
}
会有几个输出?

forkdemo3.c -- 分辨父进程和子进程

/* forkdemo3.c - shows how the return value from fork()
 *          allows a process to datermin whether
 *          is a child or process
 */

#include <stdio.h>
#include <unistd.h>
#define elseif else if

int main()
{
    int fork_rv;
    printf("Before: my pid is %d\n", getpid());
    fork_rv = fork();

    if (fork_rv == -1)
        perror("fork");
    elseif(fork_rv == 0)
        printf("I am the child. my pid = %d\n", getpid());
    else
        printf("I am the parent. my child is %d\n", fork_rv);
    return 0;
}
  fork
目标 创建进程
头文件 #include <unistd.h>
原型 pid_t result = fork(void)
参数
返回值 0 返回到子进程, -1 错误
  pid 将子进程的进程ID传给父进程

问题3: 父进程如何等待子进程退出

答案: 进程调用 wait 等待子进程结束 用户: pid = wait(&status);

系统调用 wait 做两件事. 首先, wait 暂停调用它的进程知道子进程结束. 然后, wait 取得子进程结束时传给 exit 的值.

waitdemo1.c --- 通知

/* waitdemo1.c - shows how parent pause until child finishes
 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

#define DELAY 2

int main()
{
    int newpid;
    void child_code(int), parent_code(int);
    printf("before: mypid is %d\n", getpid());

    if ((newpid = fork()) == -1)
        perror("fork");
    else if (newpid == 0)
        child_code(DELAY);
    else
        parent_code(newpid);
    return 0;
}

void child_code(int delay)
{
    printf("child %d here. will sleep for %d seconds \n", getpid(), delay);
    sleep(delay);
    printf("child done. about to exit\n");
    exit(17);
}

void parent_code(int childpid)
{
    int wait_rv;
    wait_rv = wait(NULL);
    printf("done waiting for %d. Wait returned: %d\n", childpid, wait_rv);
}

waitdemo1.c 程序体现了 wait 的两个重要特征:

  1. wait 阻塞调用它的程序知道子进程结束
  2. wait 返回结束进程的 PID

父进程调用 wait 时传入一个整形变量地址给函数. 内核将子进程的退出状态保存在 这个变量中;. 如果进程调用 exit 退出, 那么内核把 exit 的返回值存放到这个整数 变量中;如果进程是被杀死, 那么内核将信号序号放在这个变量中.

这个证书由 3 部分组成 ---- 8 个 bit 记录退出值, 7 个 bit 记录信号序号, 另一个 bit 用来指明发生错误产生了内核转储(core dump)

waitdemo2.c

/* waitdemo2.c - shows how parent gets child status
 */
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

#define DELAY 5

int main()
{
    int newpid;
    void child_code(int), parent_code(int);

    printf("before: mypid is %d\n", getpid());

    if ((newpid = fork()) == -1)
        perror("fork");
    else if (newpid == 0)
        child_code(DELAY);
    else
        parent_code(newpid);
    return 0;
}

void child_code(int delay)
{
    printf("child %d here. will sleep for %d seconds\n", getpid(), delay);
    sleep(delay);
    printf("child done. about to exit\n");
    exit(17);
}

void parent_code(int childpid)
{
    int wait_rv;
    int child_status;
    int high_8, low_7, bit_7;

    wait_rv = wait(&child_status);

    printf("done waiting for %d. Wait returned: %d\n", childpid, wait_rv);

    high_8 = child_status >> 8;  /* 1111 1111 0000 0000 */
    low_7 = child_status & 0x7F; /* 0000 0000 0111 1111 */
    bit_7 = child_status & 0x80; /* 0000 0000 1000 0000 */
    printf("status: exit=%d, sig=%d, core=%d\n", high_8, low_7, bit_7);
}

  wait
目标 等待进程结束
头文件 #include <sys/types.h>
  #include <sys/wait.h>
函数原型 pid_t result = wait(int *statusptr);
参数 statusptr 子进程的运行结果
返回值 -1 遇到错误, pid 结束进程的id
相关内容 waitpid(2), wait3(2)

实现一个 shell: psh2.c

/* prompting shell version 2
 * Solves the 'one-shot' problem of version 1
 *  Uses execvp(), but fork()s first so that
 *  the shell waits around to perform another
 *  command
 * New problem: shell catches signals. Run vi, press ^C
 */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>

#define MAXARGS 20
#define ARGLEN 100

void execute(char **);

int main()
{
    char *arglist[MAXARGS+1];
    int numargs;
    char argbuf[ARGLEN];
    char *makestring();

    numargs = 0;
    while(numargs < MAXARGS)
    {
        printf("Arg[%d]? ", numargs);
        if (fgets(argbuf, ARGLEN, stdin) && *argbuf != '\n')
            arglist[numargs++] = makestring(argbuf);
        else
        {
            if (numargs > 0){
                arglist[numargs] = NULL;
                execute(arglist);
                numargs = 0;
            }
        }
    }
    return 0;
}

void execute(char * arglist[])
{
    int pid, exitstatus;

    pid = fork();
    switch(pid){
        case -1:
            perror("fork failed");
            exit(1);
        case 0:
            execvp(arglist[0], arglist);
            perror("execvp failed");
            exit(1);
        default:
            while(wait(&exitstatus) != pid)
                ;
            printf("child exited with status %d, %d\n",
                    exitstatus >> 8, exitstatus & 0x7F);
    }
}

char *makestring(char *buf)
{
    char *cp;
    buf[strlen(buf) - 1] = '\0';
    cp = malloc(strlen(buf) + 1);
    if (cp == NULL){
        fprintf(stderr, "no memory\n");
        exit(1);
    }
    strcpy(cp, buf);
    return cp;

}

退出 psh2 的唯一方法是 Ctrl-C 键. 如果在 psh2 等待子进程结束时键入Ctrl-C, 子进程结束, 但是 shell 也结束了. 按下 Ctrl -C 所产生的 SIGINT 信号不但杀 死了运行的机场南横, 也杀死了运行的 psh2 的进程.

思考: 用进程编程

考虑下函数和进程之间的相似性

execvp/exit 就像 call/return

(1) call/return

一个 C 程序有很多函数组成. 一个函数可以调用另一个函数, 同事传给它一些参数. 被调用的函数执行一定操作, 然后返回一个值. 每个函数都有它的局部变量, 不同的 函数通过 call/return 系统进行通信

这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础. Unix 鼓励将这种应用与程序之内的模式扩展到程序之间.

(2) exec/exit

一个 C 程序可以 fork/exec 另一个程序, 并传给它一些参数. 这个被调用的程序 执行一定的操作, 然后通过 exit(n) 来返回值. 调用它的进程可以通过 wait(&result) 来获取 exit 的返回值. 子程序的 exit 返回值可以在 result 的 8 ~ 15 位之间找到.

函数调用所用到的堆栈几乎没有限制. 一个被调用的程序还可以调用其他程序, 一个通过 fork/exec 调用起来的程序可以通过 fork/exec 调用别的程序. Unix 使创建一个新进程方便而且快捷. 用 fork/exit 和 exit/wait 来调用程序 的返回结果不仅使用与shell, Unix 程序经常被设计成一组子程序, 而不是一个带有 很多函数的大程序.

由 exec 传递的参数必须是字符串. 由于进程间通信的参数为字符串, 这样就强迫子 程序的通信也必须使用文本作为参数类型. 几乎是偶然, 这种基于文本的程序接口 支持跨平台的交互, 而这一点非常重要.

全局变量和 fork/exec

全局变量是有害的, 它破坏的封装原则, 导致出人意料的副作用和难以维护的代码. 去掉全局变量确更糟糕.

Unix 提供方法来建立全局变量. 环境(environment)是一些传递给进程的字符串型 变量集合. 不会有副作用, 它对 fork/exec 和 exit/wait 机制是一个有用的补充.

exit 和 exec 的其他细节

进程死亡: exit 和 _exit

exit 是 fork 的逆操作, 进程通过调用 exit 来停止运行. fork 创建一个进程, exit 删除进程.

exit 刷新所有的流, 调用由 atexit 和 on_exit 注册的函数, 执行当前系统定义的 其他与 exit 相关的操作. 然后调用 _exit. 系统函数 _exit 是一个内核操作, 这个操作处理所有非配给这个进程的内存, 关闭所有这个进程打开的文件, 释放所有 这内核用来管理和维护这个进程的数据结构.

那个进程的弥留之言(子进程传递给 exit 的参数)被存放在内核知道这个进程的父 进程通过 wait 系统调用取回这个值. 如果父进程没有在等这个值, 那么它将被保存 在内核知道父进程调用 wait, 那时内核将通告这个父进程的子进程结束, 并转达子 进程的弥留之言.

那些已死亡但是还没有给 exit 赋值的进程被称之为僵尸(zombie)进程, 很多较新的 版本的 ps 列出这些进程并标记为 defunct.

_exit() 小结如下

系统调用 _exit 终止当前进程并执行所有必须的清理工作. 这些工作在各个不同的 版本的 Unix 中有些不同, 但都包以下一些操作:

  1. 关闭所有文件描述符和目录描述符
  2. 该进程的 PID 置为 init 进程的 PID
  3. 如果父进程调用 wait 或 waitpid 来等待子进程结束, 则通知父进程
  4. 向父进程发送 SIGCHLD.
  _exit
目标 终止当前进程
头文件 #include <unistd.h>
  #include <stdlib.h>
函数原型 void _exit(int status)
参数 status 返回值
返回值
相关内容 atexit(3), exit(3), on_exit(3)

如果父进程在子进程之前退出, 那么子进程将能继续运行, 而不会称为"孤儿", 它们将由 init 进程收养. 注意, 就算父进程没有调用 wait, 内核也会向他发送 SIGCHLD 消息. 尽管 SIGCHLD 消息的默认处理方法是忽略的.

exec 家族

execvp 不是一个系统调用, 它是一个库函数, 这个函数通过系统调用 execve 来 调用内核服务. execve 中的 e 代表环境(environment)

还有一些调用 execve 的函数是有用的

execlp(file, argv0, argv1, ..., NULL)

execlp 不想 exevp 那样需要一个参数数组. 相反, 传给 main 的 argv[] 中包含 的参数被简单的放在 execlp 参数中:

execlp("ls", "ls", "-a", "demodir", NULL);

以指定的参数运行程序ls. 当预先知道要运行的命令和它的参数时 execlp 是有用的. 但是在 shell 中,这个函数没什么用, 因为在用户输入命令之前不知道有多少个参数.

execl(fullpath, argv0, argv1, ..., NULL);

execlp 和 execvp 中的 p 代表路径( path), 这两个函数在环境变量 PATH 中列出的 路径中查找由第一个参数指定的程序. 如果准确的知道这个文件的位置, 那么就能够 在 execl 中的第一个参数中指定它的完整路径

exec("/bin/ls", "ls", "-a", "demodir", NULL);

以指定的参数来运行程序 /bin/ls. 指定程序的准确位置比运行 execlp 更快.

使用 execv 或 execl 来执行明确指定的程序币依赖阿百川的路径列表 PATH 更安全. 因为 PATH 很容易被恶意的用户篡改.